﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Collections.Specialized;
using Quartz.Impl;
using Quartz;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Windows.Forms;
using System.IO;
using System.Xml.Linq;
using Quartz.Impl.Matchers;
using Quartz.Impl.Triggers;
using Quartz.Util;
using XiaoyuI.CrystalQuartz.Domain;
using XiaoyuI.CrystalQuartz.Domain.Activities;
using XiaoyuI.CrystalQuartz;

namespace XiaoyuI.Quartz.WinManager
{
	public class QuartzScheduler
	{
		public QuartzScheduler(string server, int port, string scheduler)
		{
			Address = string.Format("tcp://{0}:{1}/{2}", server, port, scheduler);

			_schedulerFactory = new StdSchedulerFactory(getProperties(Address));

			try
			{
				_scheduler = _schedulerFactory.GetScheduler();
			}
			catch (SchedulerException)
			{
				MessageBox.Show("Unable to connect to the specified server", "Connection Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
			}
		}
		public string Address { get; private set; }
		private NameValueCollection getProperties(string address)
		{
			NameValueCollection properties = new NameValueCollection();
			properties["quartz.scheduler.instanceName"] = "RemoteClient";
			properties["quartz.scheduler.proxy"] = "true";
			properties["quartz.threadPool.threadCount"] = "0";
			properties["quartz.scheduler.proxy.address"] = address;
			return properties;
		}
		public IScheduler GetScheduler()
		{
			return _scheduler;
		}
		public DataTable GetJobs()
		{
			DataTable table = new DataTable();
			table.Columns.Add("GroupName");
			table.Columns.Add("JobName");
			table.Columns.Add("JobDescription");
			table.Columns.Add("TriggerName");
			table.Columns.Add("TriggerGroupName");
			table.Columns.Add("TriggerType");
			table.Columns.Add("TriggerState");
			table.Columns.Add("NextFireTime");
			table.Columns.Add("PreviousFireTime");
			var jobGroups = GetScheduler().GetJobGroupNames();
			foreach (string group in jobGroups)
			{
				var groupMatcher = GroupMatcher<JobKey>.GroupContains(group);
				var jobKeys = GetScheduler().GetJobKeys(groupMatcher);
				foreach (var jobKey in jobKeys)
				{
					var detail = GetScheduler().GetJobDetail(jobKey);
					var triggers = GetScheduler().GetTriggersOfJob(jobKey);
					foreach (ITrigger trigger in triggers)
					{
						DataRow row = table.NewRow();
						row["GroupName"] = group;
						row["JobName"] = jobKey.Name;
						row["JobDescription"] = detail.Description;
						row["TriggerName"] = trigger.Key.Name;
						row["TriggerGroupName"] = trigger.Key.Group;
						row["TriggerType"] = trigger.GetType().Name;
						row["TriggerState"] = GetScheduler().GetTriggerState(trigger.Key);
						DateTimeOffset? nextFireTime = trigger.GetNextFireTimeUtc();
						if (nextFireTime.HasValue)
						{
							row["NextFireTime"] = TimeZone.CurrentTimeZone.ToLocalTime(nextFireTime.Value.DateTime);
						}

						DateTimeOffset? previousFireTime = trigger.GetPreviousFireTimeUtc();
						if (previousFireTime.HasValue)
						{
							row["PreviousFireTime"] = TimeZone.CurrentTimeZone.ToLocalTime(previousFireTime.Value.DateTime);
						}

						table.Rows.Add(row);
					}
				}
			}
			return table;
		}


		private ISchedulerFactory _schedulerFactory;

		private IScheduler _scheduler;

		public void ScheduleOneTimeJob(Type jobType, JobDataMap dataMap, int clientID)
		{
			string name = string.Format("{0}-{1}", jobType.Name, clientID);
			string group = clientID.ToString();
			IJobDetail jobDetail = JobBuilder.
				Create().
				OfType(jobType).
				WithIdentity(name, group).
				WithDescription("One time job").
				UsingJobData(dataMap).Build();
			ITrigger trigger = TriggerBuilder.
				Create().
				ForJob(jobDetail).
				WithIdentity(name, group).
				WithSchedule(SimpleScheduleBuilder.Create().WithRepeatCount(0).WithInterval(TimeSpan.Zero)).
				StartNow().Build();
			GetScheduler().ScheduleJob(jobDetail, trigger);
		}
		public DataTable GetRunningJobs()
		{
			DataTable table = new DataTable();
			table.Columns.Add("JobName", typeof(string));
			table.Columns.Add("RunTime", typeof(int));
			try
			{
				var contexts = GetScheduler().GetCurrentlyExecutingJobs();
				foreach (var context in contexts)
				{
					DataRow row = table.NewRow();
					row["JobName"] = context.JobDetail.Key.Name;
					row["RunTime"] = (DateTime.Now.ToUniversalTime() - ((DateTimeOffset)context.FireTimeUtc).DateTime).TotalMinutes;
					table.Rows.Add(row);
				}
			}
			catch (Exception ex)
			{
				//TODO: Let the user know we couldn't load the running jobs.
			}

			return table;
		}

		public void BackupToFile(System.IO.FileInfo file)
		{
			IScheduler scheduler = GetScheduler();
			var jobGroupNames = scheduler.GetJobGroupNames();
			List<IJobDetail> jobDetails = new List<IJobDetail>();
			foreach (var jobGroup in jobGroupNames)
			{
				var groupMatcher = GroupMatcher<JobKey>.GroupContains(jobGroup);

				var jobKeys = scheduler.GetJobKeys(groupMatcher);
				foreach (var jobKey in jobKeys)
				{
					jobDetails.Add(scheduler.GetJobDetail(jobKey));
				}
			}
			writeToFile(file, jobDetails);

		}

		private void writeToFile(System.IO.FileInfo file, List<IJobDetail> jobDetails)
		{
			using (StreamWriter writer = file.CreateText())
			{
				XNamespace ns = "http://quartznet.sourceforge.net/JobSchedulingData";
				XDocument doc = new XDocument(new XDeclaration("1.0", "UTF-8", "yes")
					, new XElement(ns + "quartz"
						, new XAttribute(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance")
						, new XAttribute("version", "1.0")
						, new XAttribute("overwrite-existing-jobs", "true")
						)
					);
				foreach (IJobDetail detail in jobDetails)
				{
					doc.Root.Add(
							new XElement(ns + "job"
							, new XElement(ns + "job-detail"
							, new XElement(ns + "name", detail.Key.Name)
							, new XElement(ns + "group", detail.Key.Group)
							, new XElement(ns + "description", detail.Description)
							, new XElement(ns + "job-type", detail.JobType.FullName + "," + detail.JobType.Assembly.FullName)
						//TODO: Apparently volatile is no longer available. Check.
						//, new XElement(ns + "volatile", detail.Volatile)
							, new XElement(ns + "durable", detail.Durable)
							, new XElement(ns + "recover", detail.RequestsRecovery)
							, getJobDataMap(ns, detail.JobDataMap)
							)
							, getTriggers(ns, detail)
							)
						);
				}
				writer.Write(doc);
				writer.Flush();
				writer.Close();
			}
		}

		private XElement getJobDataMap(XNamespace ns, JobDataMap jobDataMap)
		{
			XElement map = new XElement(ns + "job-data-map");
			foreach (var key in jobDataMap.GetKeys())
			{
				map.Add(new XElement(ns + "entry"
					, new XElement(ns + "key", key)
					, new XElement(ns + "value", jobDataMap[key])
					)
				);
			}

			return map;
		}

		private XElement[] getTriggers(XNamespace ns, IJobDetail detail)
		{
			var triggers = _scheduler.GetTriggersOfJob(detail.Key);
			XElement[] elements = new XElement[triggers.Count];
			int i = 0;
			foreach (var trigger in triggers)
			{
				elements[i] = new XElement(ns + "trigger");
				if (triggers[i] is SimpleTriggerImpl)
				{
					elements[i].Add(getSimpleTrigger(ns, (SimpleTriggerImpl)triggers[i]));
				}
				else if (triggers[i] is CronTriggerImpl)
				{
					elements[i].Add(getCronTrigger(ns, (CronTriggerImpl)triggers[i]));
				}
				i++;
			}
			return elements;
		}

		private XElement getCronTrigger(XNamespace ns, CronTriggerImpl trigger)
		{
			XElement cronTrigger = new XElement(ns + "cron");
			addCommonTriggerData(ns, cronTrigger, trigger);
			cronTrigger.Add(
				new XElement(ns + "cron-expression", trigger.CronExpressionString)
				);
			return cronTrigger;
		}

		private void addCommonTriggerData(XNamespace ns, XElement rootTriggerElement, AbstractTrigger trigger)
		{
			rootTriggerElement.Add(
				new XElement(ns + "name", trigger.Key.Name)
				, new XElement(ns + "group", trigger.Key.Group)
				, new XElement(ns + "description", trigger.Description)
				, new XElement(ns + "misfire-instruction", getMisfireInstructionText(trigger))
				//, new XElement(ns + "volatile", trigger.Volatile)
				, new XElement(ns + "job-name", trigger.JobName)
				, new XElement(ns + "job-group", trigger.JobGroup)
				);
		}

		private string getMisfireInstructionText(AbstractTrigger trigger)
		{
			if (trigger is CronTriggerImpl)
			{
				return getCronTriggerMisfireInstructionText(trigger.MisfireInstruction);
			}
			return getSimpleTriggerMisfireInstructionText(trigger.MisfireInstruction);
		}

		private string getSimpleTriggerMisfireInstructionText(int misfireInstruction)
		{
			switch (misfireInstruction)
			{
				case 0:
					return "SmartPolicy";
				case 1:
					return "FireNow";
				case 2:
					return "RescheduleNowWithExistingRepeatCount";
				case 3:
					return "RescheduleNowWithRemainingRepeatCount";
				case 4:
					return "RescheduleNextWithRemainingCount";
				case 5:
					return "RescheduleNextWithExistingCount";
				default:
					throw new ArgumentOutOfRangeException(string.Format("{0} is not a supported misfire instruction for SimpleTrigger See Quartz.MisfireInstruction for more details.", misfireInstruction));
			}
		}

		private string getCronTriggerMisfireInstructionText(int misfireInstruction)
		{
			switch (misfireInstruction)
			{
				case 0:
					return "SmartPolicy";
				case 1:
					return "FireOnceNow";
				case 2:
					return "DoNothing";
				default:
					throw new ArgumentOutOfRangeException(string.Format("{0} is not a supported misfire instruction for CronTrigger See Quartz.MisfireInstruction for more details.", misfireInstruction));
			}
		}

		private XElement getSimpleTrigger(XNamespace ns, SimpleTriggerImpl trigger)
		{
			XElement simpleTrigger = new XElement(ns + "simple");
			addCommonTriggerData(ns, simpleTrigger, trigger);
			simpleTrigger.Add(
				new XElement(ns + "repeat-count", trigger.RepeatCount)
				, new XElement(ns + "repeat-interval", trigger.RepeatInterval.Milliseconds)
				);
			return simpleTrigger;
		}

		public SchedulerData GetSchedulerData()
		{
			IScheduler scheduler = _scheduler;
			SchedulerMetaData metadata = scheduler.GetMetaData();

			IList<ExecutingJobInfo> inProgressJobs = metadata.SchedulerRemote ? (IList<ExecutingJobInfo>)new ExecutingJobInfo[0] :
				scheduler
					.GetCurrentlyExecutingJobs()
					.Select(x => new ExecutingJobInfo
					{
						UniqueTriggerKey = x.Trigger.Key.ToString(),
						FireInstanceId = x.FireInstanceId
					})
					.ToList();

			return new SchedulerData
			{
				Name = scheduler.SchedulerName,
				InstanceId = scheduler.SchedulerInstanceId,
				JobGroups = GetJobGroups(scheduler),
				Status = GetSchedulerStatus(scheduler),
				JobsExecuted = metadata.NumberOfJobsExecuted,
				JobsTotal = scheduler.IsShutdown ? 0 : scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup()).Count,
				RunningSince = metadata.RunningSince.ToDateTime(),
				InProgress = inProgressJobs
			};
		}
		public SchedulerStatus GetSchedulerStatus(IScheduler scheduler)
		{
			if (scheduler.IsShutdown)
			{
				return SchedulerStatus.Shutdown;
			}

			var jobGroupNames = scheduler.GetJobGroupNames();
			if (jobGroupNames == null || jobGroupNames.Count == 0)
			{
				return SchedulerStatus.Empty;
			}

			if (scheduler.InStandbyMode)
			{
				return SchedulerStatus.Ready;
			}

			if (scheduler.IsStarted)
			{
				return SchedulerStatus.Started;
			}

			return SchedulerStatus.Ready;
		}
		private static IList<JobGroupData> GetJobGroups(IScheduler scheduler)
		{
			var result = new List<JobGroupData>();

			if (!scheduler.IsShutdown)
			{
				foreach (var groupName in scheduler.GetJobGroupNames())
				{
					var groupData = new JobGroupData(
						groupName,
						GetJobs(scheduler, groupName));

					result.Add(groupData);
				}
			}

			return result;
		}

	 
		public JobDetailsData GetJobDetailsData(string name, string group)
		{
			var scheduler = _scheduler;
			if (scheduler.IsShutdown)
			{
				return null;
			}

			IJobDetail job;

			try
			{
				job = scheduler.GetJobDetail(new JobKey(name, @group));
			}
			catch (Exception)
			{
				// GetJobDetail method throws exceptions for remote 
				// scheduler in case when JobType requires an external 
				// assembly to be referenced.
				// see https://github.com/guryanovev/CrystalQuartz/issues/16 for details

				return new JobDetailsData(null, null);
			}

			if (job == null)
			{
				return null;
			}

			return new JobDetailsData(
				GetJobDetails(job),
				job.JobDataMap.ToDictionary(x => x.Key, x => x.Value));
		}

		private JobDetails GetJobDetails(IJobDetail job)
		{
			return new JobDetails
			{
				ConcurrentExecutionDisallowed = job.ConcurrentExecutionDisallowed,
				Description = job.Description,
				Durable = job.Durable,
				JobType = job.JobType,
				PersistJobDataAfterExecution = job.PersistJobDataAfterExecution,
				RequestsRecovery = job.RequestsRecovery
			};
		}

		public SchedulerDetails GetSchedulerDetails()
		{
			IScheduler scheduler = _scheduler;
			SchedulerMetaData metadata = scheduler.GetMetaData();

			return new SchedulerDetails
			{
				InStandbyMode = metadata.InStandbyMode,
				JobStoreClustered = metadata.JobStoreClustered,
				JobStoreSupportsPersistence = metadata.JobStoreSupportsPersistence,
				JobStoreType = metadata.JobStoreType,
				NumberOfJobsExecuted = metadata.NumberOfJobsExecuted,
				RunningSince = metadata.RunningSince.ToUnixTicks(),
				SchedulerInstanceId = metadata.SchedulerInstanceId,
				SchedulerName = metadata.SchedulerName,
				SchedulerRemote = metadata.SchedulerRemote,
				SchedulerType = metadata.SchedulerType,
				Shutdown = metadata.Shutdown,
				Started = metadata.Started,
				ThreadPoolSize = metadata.ThreadPoolSize,
				ThreadPoolType = metadata.ThreadPoolType,
				Version = metadata.Version
			};
		}

		public TriggerDetailsData GetTriggerDetailsData(string name, string group)
		{
			var scheduler = _scheduler;
			if (scheduler.IsShutdown)
			{
				return null;
			}

			ITrigger trigger = scheduler.GetTrigger(new TriggerKey(name, group));
			if (trigger == null)
			{
				return null;
			}

			return new TriggerDetailsData
			{
				PrimaryTriggerData = GetTriggerData(scheduler, trigger),
				SecondaryTriggerData = GetTriggerSecondaryData(trigger),
				JobDataMap = trigger.JobDataMap.ToDictionary(x => x.Key, x => x.Value)
			};
			}

			public IEnumerable<Type> GetScheduledJobTypes()
		{
			var scheduler = _scheduler;
			if (scheduler.IsShutdown)
			{
				return new Type[0];
			}

			return scheduler
				.GetJobKeys(GroupMatcher<JobKey>.AnyGroup())
				.Select(key => scheduler.GetJobDetail(key).JobType);
		}

		private static TriggerSecondaryData GetTriggerSecondaryData(ITrigger trigger)
		{
			return new TriggerSecondaryData
			{
				Description = trigger.Description,
				Priority = trigger.Priority,
				MisfireInstruction = trigger.MisfireInstruction,

			};
		}
		private static IList<JobData> GetJobs(IScheduler scheduler, string groupName)
		{
			var result = new List<JobData>();

			foreach (var jobKey in scheduler.GetJobKeys(GroupMatcher<JobKey>.GroupEquals(groupName)))
			{
				result.Add(GetJobData(scheduler, jobKey.Name, groupName));
			}

			return result;
		}

		private static JobData GetJobData(IScheduler scheduler, string jobName, string group)
		{
			return new JobData(jobName, group, GetTriggers(scheduler, jobName, group));
		}

		private static IList<TriggerData> GetTriggers(IScheduler scheduler, string jobName, string group)
		{
			return scheduler
				.GetTriggersOfJob(new JobKey(jobName, group))
				.Select(trigger => GetTriggerData(scheduler, trigger))
				.ToList();
		}
		private static readonly TriggerTypeExtractor TriggerTypeExtractor = new TriggerTypeExtractor();
		private static TriggerData GetTriggerData(IScheduler scheduler, ITrigger trigger)
		{
			return new TriggerData(
				trigger.Key.ToString(),
				trigger.Key.Group,
				trigger.Key.Name,
				GetTriggerStatus(trigger, scheduler),
				trigger.StartTimeUtc.UtcDateTime,
				trigger.EndTimeUtc?.UtcDateTime,
				trigger.GetNextFireTimeUtc()?.UtcDateTime,
				trigger.GetPreviousFireTimeUtc()?.UtcDateTime,
				TriggerTypeExtractor.GetFor(trigger));
		}

		private static ActivityStatus GetTriggerStatus(string triggerName, string triggerGroup, IScheduler scheduler)
		{
			var state = scheduler.GetTriggerState(new TriggerKey(triggerName, triggerGroup));
			switch (state)
			{
				case TriggerState.Paused:
					return ActivityStatus.Paused;
				case TriggerState.Complete:
					return ActivityStatus.Complete;
				default:
					return ActivityStatus.Active;
			}
		}

		private static ActivityStatus GetTriggerStatus(ITrigger trigger, IScheduler scheduler)
		{
			return GetTriggerStatus(trigger.Key.Name, trigger.Key.Group, scheduler);
		}
	}
}
